﻿using CookComputing.XmlRpc;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Configuration;
using System.Reflection;

namespace Jlob.OpenErpNet
{
    public class OpenErpService
    {
        private OpenErpServiceContext context;
        private const string UrlLogin = "/xmlrpc/common";
        private const string UrlClient = "/xmlrpc/object";
        private const string DbNameOption = "OpenErpDbName";
        private const string UserNameOption = "OpenErpUser";
        private const string PwdOption = "OpenErpPwd";
        private const string UrlOption = "OpenErpUrl";

        #region Constructors
        /// <summary>
        /// Takes configuration from app.config parameters
        /// </summary>
        public OpenErpService()
        {
            context = new OpenErpServiceContext();
            context.OpenErpLogin = XmlRpcProxyGen.Create<IOpenErpLogin>();
            context.OpenErpClient = XmlRpcProxyGen.Create<IOpenErp>();
            if (!String.IsNullOrEmpty(ConfigurationManager.AppSettings[UrlOption]))
            {
                context.OpenErpLogin.Url = ConfigurationManager.AppSettings[UrlOption] + UrlLogin;
                context.OpenErpClient.Url = ConfigurationManager.AppSettings[UrlOption] + UrlClient;
            }
            if (!String.IsNullOrEmpty(ConfigurationManager.AppSettings[DbNameOption]))
            {
                context.DbName = ConfigurationManager.AppSettings[DbNameOption];
            }
            if (!String.IsNullOrEmpty(ConfigurationManager.AppSettings[UserNameOption]))
            {
                context.User = ConfigurationManager.AppSettings[UserNameOption];
            }
            if (!String.IsNullOrEmpty(ConfigurationManager.AppSettings[PwdOption]))
            {
                context.Pwd = ConfigurationManager.AppSettings[PwdOption];
            }
        }

        /// <summary>
        /// Createas a new instance of OpenErpService
        /// </summary>
        /// <param name="url">OpenErp access url, ej: http://192.168.2.1:8069</param>
        public OpenErpService(string url)
            :this()
        {
            this.context.OpenErpClient.Url = url + UrlClient;
            this.context.OpenErpLogin.Url = url + UrlLogin;
        }

        /// <summary>
        /// Creates a new instance of OpenErpService
        /// </summary>
        /// <param name="url">OpenErp instance access url, ej: http://192.168.2.1:8069</param>
        /// <param name="dbName">Database name to connect to OpenErp instance</param>
        /// <param name="userName">Username to connect to OpenErp instance</param>
        /// <param name="pwd">Password to connect to OpenErp instance</param>
        public OpenErpService(string url, string dbName, string userName, string pwd)
            :this(url)
        {
            context.DbName = dbName;
            context.User = userName;
            context.Pwd = pwd;
        }
        #endregion

        /// <summary>
        /// Retrives data from OpenErp and map into entity
        /// </summary>
        /// <typeparam name="T">Entity type</typeparam>
        /// <param name="conditions">Search conditions</param>
        /// <returns></returns>
        public T GetEntity<T>(Expression<Func<T, bool>> conditions) where T:class, new()
        {
            CommandContext context;
            T entity = default(T);
            context = CommandContextFactory.BuildCommandContextFromExpression<T>(conditions);
            IEnumerable<object> ids = SearchCommand(context);
            context.ClearArguments();
            IEnumerable<T> entities = GetEntityCommand<T>(context, ids);
            entity = entities.FirstOrDefault();
            return entity;
        }

        public IEnumerable<T> GetEntities<T>(Expression<Func<T, bool>> conditions) where T:class, new()
        {
            CommandContext context;
            T entity = default(T);
            context = CommandContextFactory.BuildCommandContextFromExpression<T>(conditions);
            IEnumerable<object> ids = SearchCommand(context);
            context.ClearArguments();
            IEnumerable<T> entities = GetEntityCommand<T>(context, ids);
            return entities;
        }

        /// <summary>
        /// Gets an entity from openerp. Reads openerpmap attribute for underlaying property type and
        /// gets from openerp.
        /// </summary>
        /// <param name="property">Property info</param>
        /// <param name="id">Id of entity to search</param>
        /// <returns>Entity object</returns>
        internal object GetEntityById(System.Reflection.PropertyInfo property, int id)
        {
            CommandContext context = new CommandContext();
            OpenErpMapAttribute[] attributes = (OpenErpMapAttribute[])property.PropertyType.GetCustomAttributes(typeof(OpenErpMapAttribute), false);
            if(attributes.Length==0){
                throw new ArgumentException("OpenErpMap not defined");
            }
            string OpenErpName = attributes[0].OpenErpName;
            Object[] resultset;
            resultset = this.context.OpenErpClient.read(this.context.DbName, this.context.UserId, this.context.Pwd, OpenErpName, "read", new object[] { id }, context.GetArguments());
            //Call a BuildEntities genereci method from this class
            MethodInfo method = this.GetType().GetMethod("BuildEntities", BindingFlags.NonPublic | BindingFlags.Instance);
            method = method.MakeGenericMethod(property.PropertyType);
            IEnumerable res = method.Invoke(this, new object[]{resultset}) as IEnumerable;
            return res.Cast<object>().First();
        }

        /// <summary>
        /// Add entity to OpenErp
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="entity"></param>
        public void AddEntity<T>(T entity) where T : class
        {
            CommandContext context;
            int id;
            context = CommandContextFactory.BuildCommandContextFromEntity<T>(entity);
            id = AddCommand(context);
            //Asignamos el id a la entidad
            SetEntityId<T>(entity, id);
        }

        /// <summary>
        /// Delete entity from OpenErp
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="entity"></param>
        public void DeleteEntity<T>(T entity) where T : class
        {
            CommandContext context;
            context = CommandContextFactory.BuildCommandContextFromEntity<T>(entity);
            DeleteCommand(context);

        }

        /// <summary>
        /// Update OpenErp entity with current entity values
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="entity"></param>
        public void UpdateEntity<T>(T entity) where T : class
        {
            CommandContext context;
            context = CommandContextFactory.BuildCommandContextFromEntity<T>(entity);
            UpdateCommand(context);
        }

        private IEnumerable<object> SearchCommand(CommandContext commandContext) 
        {
            object[] res;
            //Login
            this.context.UserId = this.context.OpenErpLogin.login(this.context.DbName, this.context.User, this.context.Pwd);
            res = this.context.OpenErpClient.search(this.context.DbName, this.context.UserId, this.context.Pwd, commandContext.EntityName, "search", commandContext.GetArguments());
            return res.ToList();
        }

        /// <summary>
        /// Execute Add command in OpenErp with context params.
        /// 
        /// Exclude id property because OpenErp will return it after creation.
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        private int AddCommand(CommandContext context)
        {
            int res;
            //Login
            this.context.UserId = this.context.OpenErpLogin.login(this.context.DbName, this.context.User, this.context.Pwd);
            //Transformamos los argumentos en XmlRpcStruct
            XmlRpcStruct fields = new XmlRpcStruct();
            foreach (CommandArgument argument in context.Arguments)
            {
                if (!argument.Property.Equals("id"))
                {
                    fields.Add(argument.Property, argument.Value);
                }
            }
            try
            {
                res = this.context.OpenErpClient.create(this.context.DbName, this.context.UserId, this.context.Pwd, context.EntityName, "create", fields);
            }
            catch (XmlRpcFaultException e)
            {
                throw OpenErpServiceException.GetException(e);
            }
            return res;
        }

        /// <summary>
        /// Executes delete command in OpenErp. Get id from context.
        /// </summary>
        /// <param name="context"></param>
        private void DeleteCommand(CommandContext context)
        {
            this.context.UserId = this.context.OpenErpLogin.login(this.context.DbName, this.context.User, this.context.Pwd);
            CommandArgument idArgument = (from a in context.Arguments
                                         where a.Property.Equals("id")
                                         select a).First();
            object[] idArray = new object[1];
            idArray[0] = idArgument.Value;
            this.context.OpenErpClient.unlink(this.context.DbName, this.context.UserId, this.context.Pwd, context.EntityName, "unlink", idArray);
        }

        private void UpdateCommand(CommandContext context)
        {
            this.context.UserId = this.context.OpenErpLogin.login(this.context.DbName, this.context.User, this.context.Pwd);
            int[] idArray = new int[1];
             //Transformamos los argumentos en XmlRpcStruct
            XmlRpcStruct fields = new XmlRpcStruct();
            foreach (CommandArgument argument in context.Arguments)
            {
                if (!argument.Property.Equals("id"))
                {
                    if (argument.Value != null)
                    {
                        fields.Add(argument.Property, argument.Value);
                    }
                }else
	            {
                    idArray[0] = (int)argument.Value;
	            }
            }
            this.context.OpenErpClient.write(this.context.DbName, this.context.UserId, this.context.Pwd, context.EntityName, "write", idArray, fields);
        }

        private IEnumerable<T> GetEntityCommand<T>(CommandContext commandContext, IEnumerable<object> ids) where T : class, new()
        {
            Object[] resultset;
            resultset = this.context.OpenErpClient.read(this.context.DbName, this.context.UserId, this.context.Pwd, commandContext.EntityName, "read", ids.ToArray(), commandContext.GetArguments());
            return BuildEntities<T>(resultset);
        }

        /// <summary>
        /// Build POCO object with data from OpenErp
        /// </summary>
        /// <typeparam name="T">Entity to build</typeparam>
        /// <param name="resultset">data returned from OpenErp</param>
        /// <returns></returns>
        private IEnumerable<T> BuildEntities<T>(object[] resultset) where T : class, new()
        {
            OpenErpEnumerable<T> entities = new OpenErpEnumerable<T>(this);
            Type typeOfT = typeof(T);
            foreach (Object item in resultset)
            {
                XmlRpcStruct xmlStruct = (XmlRpcStruct)item;
                T entity = new T();
                foreach (DictionaryEntry d in xmlStruct)
                {
                    //Buscar el attributo por nombre y assignar el valor
                    System.Reflection.PropertyInfo prop = typeOfT.GetProperties()
                                                            .FirstOrDefault(p => 
                                                                p.GetCustomAttributes(typeof(OpenErpMapAttribute), false)
                                                                .Cast<OpenErpMapAttribute>()
                                                                .Where(e => e.OpenErpName.Equals(d.Key))
                                                                .Count()==1);
                    if (prop != null)
                    {
                        OpenErpMapAttribute[] attributes;
                        attributes = (OpenErpMapAttribute[])prop.GetCustomAttributes(typeof(OpenErpMapAttribute), false);
                        if (attributes.Length > 0)
                        {
                            switch (attributes[0].OpenErpType)
                            {
                                case OpenErpType.Date:
                                    if (IsAOpenErpNull(d.Value))
                                    {
                                        prop.SetValue(entity, null, null);
                                    }
                                    else
                                    {
                                        prop.SetValue(entity, DateTime.ParseExact(d.Value.ToString(), "yyyy-MM-dd", System.Threading.Thread.CurrentThread.CurrentCulture), null);
                                    }
                                    break;
                                case OpenErpType.Datetime:
                                    if (IsAOpenErpNull(d.Value))
                                    {
                                        prop.SetValue(entity, null, null);
                                    }
                                    else
                                    {
                                        prop.SetValue(entity, DateTime.ParseExact(d.Value.ToString(), "yyyy-MM-dd HH:mm:ss", System.Threading.Thread.CurrentThread.CurrentCulture), null);
                                    }
                                    break;
                                case OpenErpType.Char:
                                    if (IsAOpenErpNull(d.Value))
                                    {
                                        prop.SetValue(entity, null, null);
                                    }
                                    else
                                    {
                                        if (prop.PropertyType.Name.Equals("DateTime"))
                                        {
                                            prop.SetValue(entity, DateTime.ParseExact(d.Value.ToString(), "yyyy-MM-dd HH:mm:ss", System.Threading.Thread.CurrentThread.CurrentCulture), null);
                                        }
                                    }
                                    break;
                                default:
                                    //Check para array de objetos
                                    object[] lista = d.Value as object[];
                                    if (lista != null)
                                    {
                                        prop.SetValue(entity, lista[0], null);
                                    }
                                    else
                                    {
                                        if (IsAOpenErpNull(d.Value))
                                        {
                                            prop.SetValue(entity, null, null);
                                        }
                                        else
                                        {
                                            prop.SetValue(entity, d.Value, null);
                                        }
                                    }
                                    break;
                            }
                        }
                    }
                }
                entities.Add(entity);
            }
            return entities;
        }

        /// <summary>
        /// Set property id from T entity. Used after insert a entity
        /// into OpenErp. OpenErp will return id of inserted entity and
        /// this method will asign to an entity's property mapped as id.
        /// </summary>
        /// <typeparam name="T">Entity type</typeparam>
        /// <param name="entity">Entity instance</param>
        /// <param name="value">Id to assign</param>
        private void SetEntityId<T>(T entity, int value)
        {
            Type t = typeof(T);
            System.Reflection.PropertyInfo prop = t.GetProperties()
                                                            .FirstOrDefault(p =>
                                                                p.GetCustomAttributes(typeof(OpenErpMapAttribute), false)
                                                                .Cast<OpenErpMapAttribute>()
                                                                .Where(e => e.OpenErpName.Equals("id"))
                                                                .Count() == 1);
            if (prop != null)
            {
                prop.SetValue(entity, value, null);
            }
        }

        /// <summary>
        /// Checks for a null value from OpenErp.
        /// OpenErp will return false for a null value.
        /// </summary>
        /// <param name="value"></param>
        private bool IsAOpenErpNull(object value)
        {
            if (value is bool && !(bool)value)
            {
                return true;
            }
            return false;
        }

    }
}
